package jtracking_client;

import jtracking_both.*;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.HashSet;


/**
 *  <p style="margin-top: 0">
 *  Kümmert sich um das Versenden der {@link Datenpaket}e
 *  und speichert eventuell nicht Versendete Pakete zwischen,
 *  die später gesendet werden.
 *
 *  extends Thread
 *        </p>
 */
// <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
// #[regen=yes,id=DCE.51591D7C-3301-6D00-9519-44E6B2F181A0]
// </editor-fold> 
public class DatenVersender extends Thread {

    /**
     * Eine liste der noch nicht verschickten Daten.
     * in einem festen Zeitabstand wird versucht, eventuell noch nicht versendete Daten
     * zu verschicken.
     *
     * Optional: wenn das Programm beendet wird (der PC heruntergefahren),
     * werden die aktuell wartenden Daten gesichert und beim nächsten Start wieder geladen
     * und verschickt.
     */
    // <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
    // #[regen=yes,id=DCE.F5626669-D26C-6F11-DA27-02E756993923]
    // </editor-fold> 
    private HashSet<Datenpaket> wartendeDaten;
    
    private final Object wartendeDatenLock=new Object();
    private final Object sendeLock = new Object();

    /**
     * spezifiziert, ob der thread versuchen soll, daten zu senden.
     * wenn auf false gesetzt sendet der thread nicht mehr und wartet auf interrupt/notify.
     */
    private boolean sendenVersuchen=true;

    /**
     * wenn auf true gesetzt wird der thread im nächsten zyklus beendet.
     */
    private boolean beenden=false;

    /**
     * aktualisierungszeit, 2 minuten
     */
    private final int aktualisierungszeit = 120000;

    /**
     * timeout für verbindungen
     */
    private int timeout=3000;

    private final int protocolVersion = 1;

    /**
     * der port, auf dem Verbunden wird.
     */
    private final int port;

    private final int standardPort = 58722;
    private TimeOutThread tot;
    public int getPort() {
        return port;
    }

    /**
     * die Adresse vom Server, auf den Verbunden wird
     */
    private String serverAddress="xampp";

    
    //wir können diesen auf den pc legen oder auf das benutzerverzeichnis
    //beim benutzerverzeichnis hätten wir den vorteil, dass ein entführender
    //Lehrer die alten Daten beim neueinloggen senden würde, wodurch der letzte
    //Benutzer der Eineit bekannt würde, falls er die Einheit ohne LAN beendet hat....
    //dann kann er sich aber eh nicht mehr einloggen, deshalb sind das max. 5 minuten
    //und davor ist er eingeloggt
    private final File einstellungsOrdner = new File("C:\\Programme\\jtracking\\");
    private final File einstellungsDatei = new File(einstellungsOrdner,"jtracking-client-einstellungen.conf");
    private final File paketSpeicherDatei = new File(einstellungsOrdner,"jtracking-client-pakete.db");

    

    public String getServerAddress() {
        return serverAddress;
    }

    public void setServerAddress(String serverAddress) {
        this.serverAddress = serverAddress;
    }


    // <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
    // #[regen=yes,id=DCE.8F7DF3BA-543A-1764-1D67-179FB4DA2BFE]
    // </editor-fold> 
    public DatenVersender (String serverAddress, int port) {
        setName("DatenVersender-MainThread");
        if(serverAddress!=null && !serverAddress.equals(""))
        {
            this.serverAddress=serverAddress;
        } else {
            System.out.println("Keine Serveradresse angegeben, benutze stattdessen Standard-Adresse \""+this.serverAddress+"\".");
        }
        if (port > 65535 || port < 1)
        {
            //ports gehen nur von 1 bist 65535, standardport behalten
            port = standardPort;
            System.out.println("Ungültiger Port angegeben, benutze Standardport "+port+".");
        }
        this.port = port;

        wartendeDaten=new HashSet<Datenpaket>();

        //gesicherten PublicKey von Server einlesen
        //gesicherten PrivateKey einlesen
        einstellungenLesen();
        //paketeLesen();


        //DeathHook:
        Runtime.getRuntime().addShutdownHook(new Thread(){
            @Override
            public void run()
            {
                beenden();
            }
        });
    }

    private void einstellungenSpeichern()
    {
        einstellungsOrdner.mkdirs();
        try {
            einstellungsDatei.createNewFile();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        if(einstellungsDatei.canWrite()) {
            ObjectOutputStream oos = null;
            try {
                oos = new ObjectOutputStream(new FileOutputStream(einstellungsDatei));
                oos.writeInt(protocolVersion);

            } catch (FileNotFoundException ex) {
                ex.printStackTrace();
            } catch (IOException ex) {
                ex.printStackTrace();
            } finally {
                try {
                    oos.flush();
                    oos.close();
                    oos=null;
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        } else {
            //Kann einstellungen nicht speichern. evtl log an server schicken
        }
    }

    private void einstellungenLesen()
    {
        //hier wird noch nix gespeichert.
        ObjectInputStream is=null;
        if(einstellungsDatei.canRead()) {
            try {
                is = new ObjectInputStream(new FileInputStream(einstellungsDatei));

                int protV = is.readInt();
                if(protV != protocolVersion)
                {
                    //Versuch es zu lesen, wenns nich geht, dann nich.
                }
                System.out.println("Einstellungen wurden eingelesen.");
            } catch (FileNotFoundException ex) {
            } catch (IOException ex) {
            } finally {
                try {
                    if(is!=null)
                        is.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        } else {
            System.out.println("Einstellungen nicht vorhanden, generiere neues KeyPair.");
        }
    }

    private void paketeSpeichern()
    {
        einstellungsOrdner.mkdirs();
        try {
            paketSpeicherDatei.createNewFile();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
        if(paketSpeicherDatei.canWrite()) {
            ObjectOutputStream sos=null;
            try {
                //initEncryption(clientKeyPair.getPrivate(), clientKeyPair.getPublic());
                sos = new ObjectOutputStream(new FileOutputStream(paketSpeicherDatei));
                //sos = new ObjectOutputStream(new CipherOutputStream(new FileOutputStream(paketSpeicherDatei),cipherenc));
                sos.writeInt(protocolVersion);
                synchronized(wartendeDatenLock){
                    int anzahlPakete = wartendeDaten.size();
                    sos.writeInt(anzahlPakete);

                    for(Datenpaket p:wartendeDaten)
                    {
                        sos.writeObject(p);
                        sos.flush();
                    }
                    sos.writeUTF("EOF");
                    sos.flush();
                    System.out.println("Pakete wurden gespeichert...");
                }
            /*} catch (NoSuchAlgorithmException ex) {
                ex.printStackTrace();
            } catch (NoSuchPaddingException ex) {
                ex.printStackTrace();
            } catch (InvalidKeyException ex) {
                ex.printStackTrace();*/
            } catch (FileNotFoundException ex) {
                ex.printStackTrace();
            } catch (IOException ex) {
                ex.printStackTrace();
            } finally {
                try {
                    //streams schließen
                    if(sos!=null)
                        sos.close();
                    sos = null;
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        } else {
            //Dann speichern wir die pakete eben nicht.
            //evtl alternativen speicherort implementieren
        }
    }

    private void paketeLesen()
    {
        ObjectInputStream is=null;
        if(paketSpeicherDatei.canRead()) {
            try {
                //gesicherte Pakete einlesen
                
                //is = new ObjectInputStream(new CipherInputStream(new FileInputStream(paketSpeicherDatei),cipherdec));
                is = new ObjectInputStream(new FileInputStream(paketSpeicherDatei));

                int protV = is.readInt();
                if(protV!=protocolVersion) {
                    //falsche version, lesen trotzdem mal versuchen
                }

                int anzahlPakete = is.readInt();
                wartendeDaten = new HashSet<Datenpaket>(anzahlPakete);
                for(int i=0;i<anzahlPakete;i++) {
                    Object read = is.readObject();
                    if(read instanceof Datenpaket) {
                        wartendeDaten.add((Datenpaket)read);
                    }
                }
                String eof = is.readUTF();
                if(!"EOF".equals(eof)) {
                    System.out.println("Fehler: Datenpakete wurden nicht richtig gespeichert.");
                }
                System.out.println("Pakete wurden erfolgreich eingelesen.");
            } catch (java.io.EOFException ex) {
                System.out.println("Paketspeicherdatei endete zu früh, keine Pakete wurden eingelesen.");
                ex.printStackTrace();
            } catch (FileNotFoundException ex) {
                ex.printStackTrace();
            } catch (IOException ex) {
                ex.printStackTrace();
            } catch (ClassNotFoundException ex) {
                ex.printStackTrace();
            } finally {
                try {
                    if(is!=null)
                        is.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }

    /**
     * Diese Methode wird ausgelöst, wenn DatenVersender gestartet wird.
     * sie versucht regelmäßig, die wartenden Daten zu verschicken.
     */
    @Override
    public void run()
    {
        //DatenVersender vorbereiten
        try {
            //kurz warten, um initialisierung vom Rest abzuwarten...
            sleep(2000);

        } catch (InterruptedException ex) {
        }
        //while schleife, sendenVersuchen und beenden checken, bei interrupt/notify aufwachen
        //wenn beenden==true, sofort sterben.
        //wenn sendenVersuchen==false, unendlich lang warten.
        //wenn beenden==false und sendenVersuchen=true, wartendeDatenSenden()

        while(!beenden)
        {
            if(sendenVersuchen){
                synchronized(sendeLock){
                    String success = wartendeDatenSenden();
                    if(success!=null)
                        System.out.println(success);
                    else
                        System.out.println("Pakete wurden erfolgreich versendet.");
                }
            }
            try {
                sleep(aktualisierungszeit);
            } catch (InterruptedException ex) {}

        }

    }

    /**
     * gibt die wartenden Daten zurück
     * diese nicht verändern, außer ein datensatz ist unnötig geworden (warum sollte er das?)
     * @return die wartenden daten als ArrayList
     */
    // <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
    // #[regen=yes,regenBody=yes,id=DCE.47C603C0-E534-B401-1E7C-2CD1F0D6F2BB]
    // </editor-fold> 
    protected HashSet<Datenpaket> getWartendeDaten () {
        return wartendeDaten;
    }

    /**
     * prüft, ob aktuell eine Verbindung zum Server möglich ist
     * @return einen Booleschen wert, der zeigt, ob eine Verbindung möglich ist
     */
    // <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
    // #[regen=yes,id=DCE.C0A87E76-DA2E-9FA3-817B-8D15E76EADC2]
    // </editor-fold> 
    protected boolean checkConnection () {
        socket=null;

        try {
            //ping geht nicht, angeblich in der windows-version nicht implementiert
            //einfaches connect und fertig.
            waiting(true);
            socket = new Socket(serverAddress, port);
            waiting(false);

            if(socket.isConnected())
                return true;
            else
                return false;
            
        } catch (java.net.ConnectException ex) {
            System.out.println("Konnte nicht mit Server verbinden");
        } catch (UnknownHostException ex) {
            System.out.println("Host "+serverAddress+" wurde nicht gefunden!");
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            try {
                if(socket!=null)
                    socket.close();
            } catch (IOException ex) { }
        }
        return false;
    }

    /**
     * Trägt ein {@link Datenpaket} in die Warteliste ein und versucht, es zu verschicken
     * @param paket das zu versendende {@link Datenpaket}
     * @return ob eine sofortige Versendung möglich war <-- weggemacht, da thread sonst blockieren würde.
     */
    // <editor-fold defaultstate="collapsed" desc=" UML Marker "> 
    // #[regen=yes,id=DCE.2686E647-5DE0-64F5-1B8E-BC33A638F9BC]
    // </editor-fold> 
    protected void datenVersenden (Datenpaket paket)
    {
        synchronized(wartendeDatenLock) {
        //paket in wartendeDaten kopieren
            wartendeDaten.add(paket);
        }
        System.out.println("Paket wurde eingetragen, Daten werden gesendet");
        //Thread starten, der sendet
        Thread t = new Thread(){
            @Override public void run(){
                //System.out.println("Warte auf Lock...");
                synchronized(sendeLock){
                    //System.out.println("Daten werden gesendet...");
                    String success = wartendeDatenSenden();
                    if(success!=null)
                        System.out.println(success);
                    else
                        System.out.println("Pakete wurden erfolgreich versendet.");
                }
            }
        };
        t.start();
    }


    private boolean waiting=false;

    private ObjectInputStream input=null;
    private ObjectOutputStream output=null;
    private Socket socket=null;


    /**
     * sendet die wartenden daten
     * darf nur im thread von datenVersender gestartet werden!
     * @return null, wenn das senden erfolgreich war, sonst eine fehlermeldung
     */
    private synchronized String wartendeDatenSenden(int timeout)
    {
        if (timeout<1)
        {
            //default-timeout
            timeout=3000;
        }
        this.timeout=timeout;
        HashSet<Datenpaket> wartendeDatenClone;
        synchronized(wartendeDatenLock){
            //überprüfen, ob etwas zu senden da ist
            if(wartendeDaten.size()==0)
                return "Keine Daten zum Senden vorhanden";
            wartendeDatenClone=new HashSet<Datenpaket>(wartendeDaten);
        }
        //hier die daten senden -- verschlüsselung -- möglichkeiten:
        //übertragung über http oder https oder eigenes protokoll (streams und sockets - yeah!)
        //mögl. zusätzliche rsa verschlüsselung nach http://www.aviransplace.com/2004/10/12/using-rsa-encryption-with-java/
        //dann muss aber java-servlet auf server laufen, sonst nur php zum eintragen in die datenbank nötig
        //javax.crypto.Cipher cipher = Cipher.getInstance(”RSA/ECB/PKCS1Padding”); wäre rsa
        if(!checkConnection())
        {
            System.out.println("Konnte keine Verbindung aufbauen, versuche es später nocheinmal.");
            return "Server nicht erreichbar / JTracking_server läuft nicht";
        }
        System.out.println("Server scheint erreichbar zu sein, verbinde...");
        

        try {
            
            //connecten
            socket = new Socket(serverAddress, port);
            waiting(true);
            output = new ObjectOutputStream(socket.getOutputStream());
            output.flush();
            waiting(false);
            waiting(true);
            input = new ObjectInputStream(socket.getInputStream());
            waiting(false);
            
            int anzahlPakete = wartendeDatenClone.size();

            waiting(true);
            output.writeInt(anzahlPakete);
            output.flush();
            waiting(false);

            waiting(true);
            output.writeObject(wartendeDatenClone);
            output.flush();
            waiting(false);
            //wenn erfolgreich, die gesendeten pakete aus wartendeDaten entfernen
            synchronized(wartendeDatenLock)
            {
                wartendeDaten.removeAll(wartendeDatenClone);
            }
            wartendeDatenClone.clear();
            wartendeDatenClone=null;

        } catch (java.net.ConnectException ex) {
            System.out.println("Konnte nicht mit Server verbinden");
        } catch (UnknownHostException ex) {
            ex.printStackTrace();
        } catch (java.net.SocketException ex) {
            ex.printStackTrace();
            return "Verbindungsfehler";
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            try {
                tot.run = false;
                tot.interrupt();
                tot.notifyAll();
                timeout(false);
            } catch (Exception ex) {}
            
        }

        return null;
    }

    /*
     * Format für Übertragung:
     * alles über ObjectOutputStream
     * Server sagt "jtracking\n"
     * Server sagt int mit hashCode seines PubKeys
     * Client sagt boolean, ob er den pubkey braucht
     * Server schickt pubkey oder nicht
     *
     * jetzt wird alles verschlüsselt gesendet:
     * Client sendet dem Server seinen PubKey
     * server sagt "numberofpackets"
     * Client gibt zahl der zu versendenden Pakete als int
     * Client schickt alle Pakete als Object
     * Server sagt "gotallbye"
     * Server beendet verbindung
     * 
     * //client hat pubkey, um eventuell später noch einen chat zu implementieren
     */

    /**
     * Schließt die verbindung, um den Thread zu entblocken.
     * @param timeout ob das ein timeout oder ein normales beenden ist
     */
    private void timeout(boolean timeout)
    {
        if(socket!=null)
            System.out.println((timeout? "Timeout - ": "") +"Verbindung mit "+socket.getInetAddress()+" "+socket.getLocalPort()+" wird beendet");
        waiting=false;
        try {
            output.flush();
            input.close();
            output.close();
            socket.close();
            input=null;
            socket=null;
        } catch (Exception ex) {
        }
        System.gc();
    }

    private void waiting(boolean b)
    {
        if(tot!=null)
        {
            tot.run=false;
            tot.interrupt();
        }
        if(b){
            tot=new TimeOutThread();
            tot.setPriority(Math.round((Thread.MAX_PRIORITY+Thread.NORM_PRIORITY)/2));
            tot.start();
        }
    }



    /**
     * gleiche methode wie {@link wartendeDatenSenden(int)} aber mit default-timeout.
     * @return ob das senden erfolgreich war
     */
    private String wartendeDatenSenden()
    {
        return wartendeDatenSenden(-1);
    }



    /**
     * hört auf zu versuchen zu senden
     * @see wiederSenden()
     */
    protected void nichtMehrSenden()
    {
        sendenVersuchen=false;
        synchronized(this){
            this.notify();
        }
    }

    /**
     * versucht wieder, zu senden
     * @see nichtMehrSenden()
     */
    protected void wiederSenden()
    {
        sendenVersuchen=true;
        synchronized(this){
            this.notify();
        }
    }

    /**
     * beendet den thread, um das programm zu beenden.
     * wenn einmal ausgeführt kann der thread nicht mehr gestartet werden!
     * @see nichtMehrSenden()
     */
    protected void beenden()
    {
        //nicht mehr senden
        sendenVersuchen=false;
        beenden=true;
        synchronized(this){
            this.notify();
        }

        //keys speichern
        
        einstellungenSpeichern();


        //letzter sendeversuch, weniger timeout, um schnell zu beenden
        String gesendet=wartendeDatenSenden(500);

        if(gesendet!=null)
        {
            System.out.println("Ergebnis des letzten Sendevorgangs: "+gesendet);
            //daten konnten nicht gesendet werden
            //daten in datei schreiben und nächstes mal in DatenVersender.run() wieder einlesen
            paketeSpeichern();
        }
        //alles versucht, kann jetzt sterben.
    }





    //utility functions
    // <editor-fold defaultstate="collapsed" desc="utility functions">

    /**
     * timeOutThread
     * sorgt für einen timeout, wenn nix mehr ankommt
     */
    class TimeOutThread extends Thread{
        boolean run=true;
        @Override
        public void run()
        {
            try {
                sleep(DatenVersender.this.timeout);
            } catch (InterruptedException ex) {}
            if(run){
                DatenVersender.this.timeout(true);
            }
        }
    }

    /**
     * very fast hash function
     * @param str
     * @return
     */
    public final int sDBMHash(String str) {
        return sDBMHash(str.getBytes());
    }

    public final int sDBMHash(byte[] b){
        int n = 0;
        int len = b.length;

        for (int i=0;i<len;i++) {
            n = b[i] + 65599 *n;
        }
        return n;
    }
    

    


    //</editor-fold>
}